﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using System.Linq.Expressions;

namespace Quail
{
    public partial class FluentAssertion : IFluentAssertion
    {
        #region fields

        private Boolean _skipQuailLog = false;
        // If we're getting complex with and/or statements, we need to keep track of this
        private FluentAssertion _parentAssert;

        // If we're combining assertions, this will determine if we're checking with && or ||
        private AndOr _andOr;

        // Description of check, will log on result
        private  String _description;

        // Option to ignore case when comparing strings
        private Boolean _ignoreStringCase = false;

        // Must *not* etc
        private Boolean _invertResult = false;

        // Source object is what you're checking, CheckingAgainst represents what you're expecting (for appropriate types of assertions)
        private object _actualValue, _expectedValue;

        // If we have/guess the type of the objects passed in
        private Type _objectType;

        // The expression used for checking. (IsNull, IsGreaterThan, etc)
        private AssertExpressionType _expressionType;

        // The LogStatus we will be passing to the quail logger on failure
        private LogStatus _logStatus;

        // The task in which the assertion is being made
        private Task _task;

        // Result of the assertion.
        private Boolean? _result;

        private Object ActualValue
        {
            get { return _expectedValue; }
           set
           {
               InvalidateResult();
               _actualValue = value;
           }
        }

        private Object ExpectedValue
        {
            get { return _expectedValue; }
            set
            {
                InvalidateResult();
                _expectedValue = value;
            }
        }

        #endregion

        #region Constructors and static creators

        protected FluentAssertion()
        {
            
        }

        protected static IAssertActual Child(FluentAssertion parent, AndOr andOr, Task task = null)
        {
            var a = new FluentAssertion();
            a._parentAssert = parent;
            a._andOr = andOr;
            if(task != null && a._task == null) a._task = task;
            return a;
        }

        public FluentAssertion(String description)
        {
            _description = description;
        }

        public FluentAssertion(String description, Object actualValue, LogStatus logStatus)
        {
            _description = description;
            _actualValue = actualValue;
            _logStatus = logStatus;
        }

        public FluentAssertion(String description, String actualValue, LogStatus logStatus)
        {
            _objectType = typeof (String);
            _description = description;
            _actualValue = actualValue;
            _logStatus = logStatus;
        }

        public static FluentAssertion Create<T>(String description, T actualValue, LogStatus logStatus)
        {
            var a = new FluentAssertion(description, actualValue, logStatus);
            
            a.SetType(typeof (T));
            
            return a;
        }


        #endregion

        #region IAssertActual


        public IAssertLevel This<T>(T myObj)
        {
            ActualValue = myObj;
            return this;
        }
        public IAssertLevel ThisString(String myString)
        {
            ActualValue = myString;
            SetType(typeof(String));
            return this;
        }

        public IAssertLevel ThisInteger(Int32 myNumber)
        {
            ActualValue = myNumber;
            SetType(typeof(Int32));
            return this;
        }

        public IAssertLevel ThisDecimal(Decimal myNumber)
        {
            ActualValue = myNumber;
            SetType(typeof(Decimal));
            return this;
        }

        public IAssertLevel ThisNumber(object myNumber)
        {
            ActualValue = myNumber;
            SetType(typeof(Decimal));
            return this;
        }

        #endregion IAssertActual

        #region IAssertLevel

        public IAssertExpression MustBe()
        {
            _logStatus = LogStatus.Fail;
            return this;
        }

        public IAssertExpression ShouldBe()
        {
            _logStatus = LogStatus.Warning;
            return this;
        }

        public IAssertExpression MightBe()
        {
            _logStatus = LogStatus.Information;
            return this;
        }

        public IAssertPreExpression Must()
        {
            _logStatus = LogStatus.Fail;
            return this;
        }

        public IAssertPreExpression Should()
        {
            _logStatus = LogStatus.Warning;;
            return this;
        }

        public IAssertPreExpression Might()
        {
            _logStatus = LogStatus.Information;
            return this;
        }

        #endregion IAssertLevel

        #region IAssertPreExpression
        
        public IAssertExpression Be()
        {
            return this;
        }

        public IAssertExpression NotBe()
        {
            _invertResult = true;
            return this;
        }

        #endregion IAssertPreExpression

        #region IAssertExpression

        public IAssertExecute Null()
        {
            _expressionType = AssertExpressionType.Null;
            return this;
        }

        public IAssertExecute NullOrEmpty()
        {
            _expressionType = AssertExpressionType.NullOrEmpty;
            return this;
        }

        public IAssertExecute EqualTo<T>(T expectedObject)
        {
            _objectType = typeof (T);
            _expectedValue = expectedObject;
            _expressionType = AssertExpressionType.EqualTo;
            return this;
        }

        public IAssertExpressionString WhileIgnoringCase()
        {
            _ignoreStringCase = true;
            return this;
        }

        public IAssertExpectedString TheSameAs()
        {
            _expressionType = AssertExpressionType.EqualTo;
            return this;
        }

        public IAssertExpectedString Contains()
        {
            return Containing();
        }

        public IAssertExpectedString Containing()
        {
            _expressionType = AssertExpressionType.Contain;
            return this;
        }

        public IAssertExpectedNumeric GreaterThan()
        {
            _expressionType = AssertExpressionType.GreaterThan;
            return this;
        }

        public IAssertExpectedNumeric GreaterThanOrEqualTo()
        {
            _expressionType = AssertExpressionType.GreaterThanOrEqualTo;
            return this;
        }

        public IAssertExpectedNumeric LessThan()
        {
            _expressionType = AssertExpressionType.LessThan;
            return this;
        }

        public IAssertExpectedNumeric LessThanOrEqualTo()
        {
            _expressionType = AssertExpressionType.LessThanOrEqualTo;
            return this;
        }

        #endregion IAssertExpression

        #region IAssertExpected

        public void That<T>(T expected, String becauseText)
        {
            
            _objectType = expected.GetType();
            ExpectedValue = expected;
            _description = becauseText;
            LogResult();
        }

        public void That(object expected, String becauseText)
        {
            ExpectedValue = expected;
            _description = becauseText;
            LogResult();
        }

        public void ThatString(string expected, String becauseText)
        {
            ExpectedValue = expected;
            SetType(typeof(String));
            _description = becauseText;
            LogResult();
        }

        public void ThatDecimal(decimal expected, String becauseText)
        {
            ExpectedValue = expected;
            SetType(typeof(decimal));
            _description = becauseText;
            LogResult();
        }

        public void ThatNumber(object expected, String becauseText)
        {
            ExpectedValue = expected;
            SetType(typeof(Decimal));
            _description = becauseText;
            LogResult();
        }

        public void ThatInteger(int expected, String becauseText)
        {
            
            ExpectedValue = expected;
            SetType(typeof(int));
            _description = becauseText;
            LogResult();
        }

        public IAssertExecute That<T>(T expected)
        {
            _objectType = expected.GetType();
            ExpectedValue = expected;
            return this;
        }

        public IAssertExecute That(object expected)
        {
            ExpectedValue = expected;
            return this;
        }

        public IAssertExecute ThatString(string expected)
        {
            ExpectedValue = expected;
            SetType(typeof(String));
            return this;
        }

        public IAssertExecute ThatDecimal(decimal expected)
        {
            ExpectedValue = expected;
            SetType(typeof(decimal));
            return this;
        }

        public IAssertExecute ThatNumber(object expected)
        {
            ExpectedValue = expected;
            SetType(typeof(Decimal));
            return this;
        }

        public IAssertExecute ThatInteger(int expected)
        {
            ExpectedValue = expected;
            SetType(typeof(int));
            return this;
        }

        #endregion IAssertExpected

        #region IAssertExecute

        public IAssertExecute ForTask(Task task)
        {
            _task = task;
            return this;
        }

        public IAssertActual Or()
        {
            return Child(this, AndOr.Or, _task);
        }

        public IAssertActual And()
        {
           return Child(this, AndOr.And, _task);    
        }


        protected Boolean AndReturnResult(String assertReason)
        {
            _description = assertReason;
            LogResult();
            return Result;
        }

        public void Because(String assertReason)
        {
            _description = assertReason;
            LogResult();
        }

        #endregion IAssertExecute

        #region private evaluation assertion code

        private void SetType(Type t)
        {
            if (_objectType != null && _objectType != t)
                return;
            if (_objectType == null)
                _objectType = t;
        }
        
        internal void SetTask(Task task)
        {
            _task = task;
        }

        private void InvalidateResult()
        {
            if (_result.HasValue)
                _result = null;
        }

        private Boolean ReadyToCheck()
        {
            return (_actualValue != null
                    && _expectedValue != null);
        }

        protected Boolean Result
        {
            get
            {
                if (!_result.HasValue)
                {
                    _result = Check();
                }
                try
                {
                    return _invertResult ? !_result.Value : _result.Value;
                }
                catch (InvalidOperationException e)
                {
                    _task.AddLog(LogStatus.FailQuail,
                                    String.Format("Exception thrown checking assertion. Message: {0} StackTrace: {1}",
                                                e.Message, e.StackTrace));
                    throw;
                }

            }
        }

#if DEBUG
        public void SkipQUailLog()
        {
            _skipQuailLog = true;
        }
        public Boolean GetResult()
        {
            return Result;
        }
        public FluentAssertion Get()
        {
            return this;
        }
#endif
        private Boolean? Check()
        {
            if (_objectType == typeof(String))
            {
                if (_objectType == typeof(String) && _ignoreStringCase)
                {
                    _actualValue = ConvertToType<String>(_actualValue).ToLower();
                    _expectedValue = ConvertToType<String>(_expectedValue).ToLower();
                }   
                return (_expectedValue == null) ?
                    GetStringFunc(_expressionType).Invoke(_actualValue) :
                    GetStringCompare(_expressionType).Invoke(_actualValue, _expectedValue);
            }
            
            if((_objectType == typeof(Int32) || _objectType == typeof(Decimal)) && _expectedValue != null)
                return GetNumberCompare(_expressionType).Invoke(_actualValue, _expectedValue);
            
            // Probably one of our classes and not a String or value type
            if (_expectedValue != null)
                return ComparePropertyEquality(_actualValue, _expectedValue,_expressionType, _objectType);
            
            // That only leaves one thing
            return GetObjectFunc(_expressionType).Invoke(_actualValue);
        }

        private void LogResult()
        {
            if(!_skipQuailLog)
                _task.AddLog(Result ? LogStatus.Pass : _logStatus, _description);
        }

        #region Generating the lamba based on expected types

        private Func<Object, Boolean> GetObjectFunc(AssertExpressionType expression)
        {
            switch (expression)
            {
                case AssertExpressionType.Null:
                    return a => a == null;
            }
            return null;
        }

        private Func<Object, Boolean> GetStringFunc(AssertExpressionType expression)
        {
            switch (expression)
            {
                case AssertExpressionType.NullOrEmpty:
                    return a => String.IsNullOrEmpty(ConvertToType<String>(a));
                case AssertExpressionType.Null:
                    return a => a == null;
            }
            return null;
        }

        private Func<Object, Object, Boolean> GetStringCompare(AssertExpressionType expression)
        {
            switch (expression)
            {
                case AssertExpressionType.Contain:
                    return (a, b) => ConvertToType<String>(a).Contains(ConvertToType<String>(b));
            }
            return null;
        }

        private Func<Object, Object, Boolean> GetNumberCompare(AssertExpressionType expression)
        {
            switch (expression)
            {
                case AssertExpressionType.EqualTo:
                    return (a, b) => ConvertToType<Decimal>(a) == ConvertToType<Decimal>(b);
                case AssertExpressionType.GreaterThan:
                    return (a, b) => ConvertToType<Decimal>(a) > ConvertToType<Decimal>(b);
                case AssertExpressionType.GreaterThanOrEqualTo:
                    return (a, b) => ConvertToType<Decimal>(a) >= ConvertToType<Decimal>(b);
                case AssertExpressionType.LessThan:
                    return (a, b) => ConvertToType<Decimal>(a) < ConvertToType<Decimal>(b);
                case AssertExpressionType.LessThanOrEqualTo:
                    return (a, b) => ConvertToType<Decimal>(a) <= ConvertToType<Decimal>(b);
            }
            return null;
        }

        private Func<Object, Object, Boolean> GetObjectCompare(AssertExpressionType expression)
        {
            switch (expression)
            {
              case AssertExpressionType.EqualTo:
                    return (a, b) => a == b;
            }
            return null;
        }

        private static T ConvertToType<T>(Object obj)
        {
            var type = typeof(T);
            if (type.IsGenericType &&
            type.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                if (obj == null)
                {
                    // This is a dirty hack, hopefully we won't run into this scenario since we control what types can come in
                    return default(T);
                }
                type = Nullable.GetUnderlyingType(type);
            }

            return (T)Convert.ChangeType(obj, type);
        }


        #endregion


        #region Enums only for fluent assertions

        private enum AssertExpressionType
        {
            Null,
            NullOrEmpty,
            EqualTo,
            GreaterThan,
            GreaterThanOrEqualTo,
            LessThan,
            LessThanOrEqualTo,
            Contain
        }

        protected enum AndOr
        {
            And,
            Or
        }

        #endregion


        private Boolean ComparePropertyEquality(object x, object y, AssertExpressionType expression, Type objectType)
        {
            foreach(var p in GetProperties(objectType))
            {
                if (GetObjectCompare(expression).Invoke(GetValue(x, p), GetValue(x, p)))
                if (GetValue(x, p) != GetValue(y, p))
                    return false;
            }
            return true;
        }

        private IEnumerable<PropertyInfo> GetProperties(Type t)
        {
            var properties = t.GetProperties(BindingFlags.Public |
                                    BindingFlags.NonPublic |
                                    BindingFlags.DeclaredOnly |
                                    BindingFlags.Instance);
            return properties.Where(p => p.PropertyType.IsValueType);
        }

        private Object GetValue(Object obj, PropertyInfo property)
        {
            return property.GetValue(obj, null);
        }

        
        #endregion private evaluation assertion code
    }
}
